// TODO: CODE REVIEW & CLEANUP!
//
// TouchTabStrip.cs
//
// Implements TouchTabStrip and related types.
//

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using HomeUX.ClientControlSupport;
using HomeUX.UiUtilities;

namespace HomeUX.Controls
{

/// <summary>
/// TODO
/// </summary>
///
[TemplatePart(Name = LayoutRootName, Type = typeof(Panel))]
[TemplatePart(Name = ItemsPresenterName, Type = typeof(ItemsPresenter))]
public class TouchTabStrip : ItemsControl, IControlHelperConnector
{
    const string LayoutRootName = "LayoutRoot";
    const string ItemsPresenterName = "ItemsPresenter";

    public static readonly DependencyProperty LeftGapWidthProperty =
        DependencyProperty.Register("LeftGapWidth", typeof(GridLength), typeof(TouchTabStrip), null);
    public static readonly DependencyProperty RightGapWidthProperty =
        DependencyProperty.Register("RightGapWidth", typeof(GridLength), typeof(TouchTabStrip), null);

    public static readonly DependencyProperty ItemMinWidthProperty =
        DependencyProperty.Register("ItemMinWidth", typeof(double), typeof(TouchTabStrip),
            new PropertyMetadata(TouchTabStrip.OnItemMinWidthPropertyChanged));

    public static readonly DependencyProperty ItemPaddingProperty =
        DependencyProperty.Register("ItemPadding", typeof(Thickness), typeof(TouchTabStrip),
            new PropertyMetadata(TouchTabStrip.OnItemPaddingPropertyChanged));

    public static readonly DependencyProperty SelectedItemDataProperty =
        DependencyProperty.Register("SelectedItemData", typeof(string), typeof(TouchTabStrip),
            new PropertyMetadata(TouchTabStrip.OnSelectedItemDataPropertyChanged));

    public static readonly DependencyProperty IsPartiallySelectedProperty =
        DependencyProperty.Register("IsPartiallySelected", typeof(bool), typeof(TouchTabStrip),
            new PropertyMetadata(TouchTabStrip.OnIsPartiallySelectedPropertyChanged));

    public static readonly DependencyProperty TabBottomSeparatorIsSuppressedProperty =
        DependencyProperty.Register("TabBottomSeparatorIsSuppressed", typeof(bool), typeof(TouchTabStrip),
            new PropertyMetadata(TouchTabStrip.OnTabBottomSeparatorIsSuppressedPropertyChanged));

    // the default style to apply to items that are of type TextBlock and which have a null Style
    public static readonly DependencyProperty DefaultTabTextStyleProperty =
        DependencyProperty.Register("DefaultTabTextStyle", typeof(Style), typeof(TouchTabStrip), null);

    Panel _layoutRoot;
	ItemsPresenter _itemsPresenter;

    // how much to truncate the bottom of a TouchButton to make it look like a tab
    double _bottomTruncation;

    // the style of a TouchButton that's used as a tab
    Style _tabButtonStyle;

    // the style of the upper line (implemented as a Rectangle) at the bottom edge of the tab strip
    Style _upperBoundaryStyle;

    // the style of the lower line (implemented as a Rectangle) at the bottom edge of the tab strip
    Style _lowerBoundaryStyle;

    Thickness _beforeSelectedTabButtonMargin;
    Thickness _selectedTabButtonMargin;
    Thickness _afterSelectedTabButtonMargin;

    /// <summary>
    /// The upper line segments at the bottom of the tab strip.
    /// </summary>
    List<Rectangle> _upperBoundaryLines = new List<Rectangle>();

    /// <summary>
    /// The lower line segments at the bottom of the tab strip.  These must be higher in z-order
	/// than corresponding line segments in <r>_upperBoundaryLines</r>.
    /// </summary>
    List<Rectangle> _lowerBoundaryLines = new List<Rectangle>();

	/// <summary>
	/// Set to <n>true</n> if <r>DeleteBoundaryLines</r> needs to be called during the next call of
	/// PerformLayout--?? -- specifically, if tabs were added or deleted (because then the
	/// mapping between tabs and <r>_upperBoundaryLines</r> and <r>_lowerBoundaryLines</r> will no
	/// longer be correct).
	/// </summary>
	bool _needToDeleteBoundaryLines;

    /// <summary>
    /// Occurs when the user selects a different item (tab), or the <r>SelectedItemData</r> is
    /// changed in some other way.
    /// </summary>
    public event EventHandler SelectedItemDataChanged;

    /// <summary>
    /// Occurs when the user selects the item (tab) which is already selected.
    /// </summary>
    public event EventHandler CurrentItemSelected;

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public GridLength LeftGapWidth
    {
        get
        {
            return (GridLength)GetValue(LeftGapWidthProperty);
        }
        set
        {
            SetValue(LeftGapWidthProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public GridLength RightGapWidth
    {
        get
        {
            return (GridLength)GetValue(RightGapWidthProperty);
        }
        set
        {
            SetValue(RightGapWidthProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public double ItemMinWidth
    {
        get
        {
            return (double)GetValue(ItemMinWidthProperty);
        }
        set
        {
            SetValue(ItemMinWidthProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public Thickness ItemPadding
    {
        get
        {
            return (Thickness)GetValue(ItemPaddingProperty);
        }
        set
        {
            SetValue(ItemPaddingProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public string SelectedItemData
    {
        get
        {
            return (string)GetValue(SelectedItemDataProperty);
        }
        set
        {
            SetValue(SelectedItemDataProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public bool IsPartiallySelected
    {
        get
        {
            return (bool)GetValue(IsPartiallySelectedProperty);
        }
        set
        {
            SetValue(IsPartiallySelectedProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public bool TabBottomSeparatorIsSuppressed
    {
        get
        {
            return (bool)GetValue(TabBottomSeparatorIsSuppressedProperty);
        }
        set
        {
            SetValue(TabBottomSeparatorIsSuppressedProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public Style DefaultTabTextStyle
    {
        get
        {
            return (Style)GetValue(DefaultTabTextStyleProperty);
        }
        set
        {
            SetValue(DefaultTabTextStyleProperty, value);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public TouchTabStrip()
    {
        DefaultStyleKey = typeof(TouchTabStrip);
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        ControlHelper.GetRequiredTemplateChild(this, LayoutRootName, out _layoutRoot);
        ControlHelper.GetRequiredTemplateChild(this, ItemsPresenterName, out _itemsPresenter);
        UiUtil.GetRequiredResource(_layoutRoot, "BottomTruncation", out _bottomTruncation);
        UiUtil.GetRequiredResource(_layoutRoot, "TabButtonStyle", out _tabButtonStyle);
        UiUtil.GetRequiredResource(_layoutRoot, "UpperBoundaryStyle", out _upperBoundaryStyle);
        UiUtil.GetRequiredResource(_layoutRoot, "LowerBoundaryStyle", out _lowerBoundaryStyle);
        UiUtil.GetRequiredResource(_layoutRoot, "BeforeSelectedTabButtonMargin", out _beforeSelectedTabButtonMargin);
        UiUtil.GetRequiredResource(_layoutRoot, "SelectedTabButtonMargin", out _selectedTabButtonMargin);
        UiUtil.GetRequiredResource(_layoutRoot, "AfterSelectedTabButtonMargin", out _afterSelectedTabButtonMargin);

        // initialize visual state
        // TODO

#if false
        int selectedItemIndex = 1;
        for (int itemIndex = 0; itemIndex < Items.Count; itemIndex++)
        {
            object item = Items[itemIndex];

            var tabButton = new TouchButton()
            {
                Style = _tabButtonStyle,
                Margin = ((itemIndex == selectedItemIndex) ? _selectedTabButtonMargin :
                    ((itemIndex < selectedItemIndex) ? _beforeSelectedTabButtonMargin :
                        _afterSelectedTabButtonMargin))
            };
        }
#endif
    }

    /// <summary>
    /// Given a set of <n>FrameworkElement</n> objects that each represent the contents of one tab
    /// in the tab strip, this method hides all but the one for which the name ("x:Name") of the
    /// tab contents element equals <r>SelectedItemData</r>.
    /// </summary>
    /// <param name="controls"></param>
    public void ManageTabSheet(params FrameworkElement[] controls)
    {
        string selectedItemData = SelectedItemData;
        foreach (Control control in controls)
        {
            if (selectedItemData == control.Name)
                control.Visibility = Visibility.Visible;
            else
                control.Visibility = Visibility.Collapsed;
        }
    }

    static void OnSelectedItemDataPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchTabStrip)d;
		target.PerformLayoutAsync();

        if (target._layoutRoot != null) // i.e. if OnApplyTemplate was called
        {
            if (target.SelectedItemDataChanged != null)
                target.SelectedItemDataChanged(target, EventArgs.Empty);
        }
    }

    static void OnItemMinWidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchTabStrip)d;
        target.PerformLayoutAsync();
    }

    static void OnItemPaddingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchTabStrip)d;
        target.PerformLayoutAsync();
    }

    static void OnIsPartiallySelectedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchTabStrip)d;
        target.PerformLayoutAsync();
    }

    static void OnTabBottomSeparatorIsSuppressedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchTabStrip)d;
        target.PerformLayoutAsync();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return false;
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        base.GetContainerForItemOverride();
        return new TouchButton();
    }

    Dictionary<object, TouchButton> _mapItemToContainer = new Dictionary<object, TouchButton>();

    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);

        var textBlock = item as TextBlock;
        if ((textBlock != null) && (textBlock.Style == null))
            textBlock.Style = DefaultTabTextStyle;

		var container = (TouchButton)element;
		container.Style = _tabButtonStyle;
		container.Width = Double.NaN;
		container.Height = Double.NaN;
		container.Content = item;
        var itemElement = item as FrameworkElement;
        if (itemElement != null)
            container.Visibility = itemElement.Visibility;
        container.Click += OnItemClick;
        _mapItemToContainer.Add(item, container);

		// if PrepareContainerForItemOverride has been called once for each item, lay out the
		// items; we do this rather than call PerformLayoutAsync because PerformLayoutAsync doesn't
		// work in design mode
        if (_mapItemToContainer.Count == Items.Count)
            PerformLayout();
    }

    protected override void ClearContainerForItemOverride(DependencyObject element, object item)
    {
        base.ClearContainerForItemOverride(element, item);
        var container = (TouchButton)element;
        container.Click -= OnItemClick;
        _mapItemToContainer.Remove(item);
        PerformLayoutAsync();
    }

    void OnItemClick(object sender, RoutedEventArgs e)
    {
        var container = (TouchButton) sender;
        string newItemData = Item.GetUntypedValue(container.Content);
        if (SelectedItemData != newItemData)
        {
            // the user clicked on a tab that wasn't already selected -- select that tab
            SelectedItemData = newItemData;
        }
        else
        {
            // the user clicked on the currently-selected tab -- fire the CurrentItemSelected
            // property
            if (CurrentItemSelected != null)
                CurrentItemSelected(this, EventArgs.Empty);
        }
    }

    protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        // the mapping between tabs and <_upperBoundaryLines> and <_lowerBoundaryLines> will no
		// longer be correct, since the Items collection (i.e. the collection of tabs) has changed
        _needToDeleteBoundaryLines = true;
    }

    bool _pendingLayout;

    void PerformLayoutAsync()
    {
        if (_layoutRoot == null)
            return; // too early for layout
        if (_pendingLayout)
            return;
        Dispatcher.BeginInvoke(PerformLayout);
        _pendingLayout = true;
    }

#if false
    [DebuggerDisplay("ElementPool: {Count} elements, {_elementsInUse} in use")]
    class ElementPool<T> where T : FrameworkElement
    {
        List<T> _elements = new List<T>();
        int _elementsInUse;

        public int Count { get { return _elements.Count; } }

        /// <summary>
        /// Allocates a recycled or new element.
        /// </summary>
        ///
        /// <param name="allocator">The method to call if a new element must be created.</param>
        ///
        public T Allocate(Func<T> allocator)
        {
            T element;
            if (_elementsInUse < _elements.Count)
                element = _elements[_elementsInUse++];
            else
            {
                element = allocator();
                _elements.Add(element);
                _elementsInUse++;
            }
            return element;
        }

        /// <summary>
        /// Marks all elements in the element pool as available for re-use.
        /// </summary>
        public void Recycle()
        {
            _elementsInUse = 0;
        }

        /// <summary>
        /// Discards any elements that are not in use.  This must be done after layout, otherwise
        /// unused elements could be visible as garbage on the screen.
        /// </summary>
        public void TrimExcess()
        {
            if (_elementsInUse < _elements.Count)
            {
                for (int elementIndex = _elementsInUse;
                     elementIndex < _elements.Count;
                     elementIndex++)
                {
                    T element = _elements[elementIndex];
                    ((Panel)element.Parent).Children.Remove(element);
                }
                _elements.RemoveRange(_elementsInUse, _elements.Count - _elementsInUse);
            }
        }
    }

    /// <summary>
    /// The upper line segments at the bottom of the tab strip.
    /// </summary>
    ElementPool<Rectangle> _upperBoundaryLines = new ElementPool<Rectangle>();

    /// <summary>
    /// The lower line segments at the bottom of the tab strip.
    /// </summary>
    ElementPool<Rectangle> _lowerBoundaryLines = new ElementPool<Rectangle>();
#endif

	/// <summary>
	/// Allocates (if needed) and adjusts the properties of a pair of boundary lines (upper and
	/// lower) that correspond to a given column of the <n>Grid</n> that contains tab buttons.
	/// </summary>
	///
	/// <param name="innerGrid">The <n>Grid</n> that contains tab buttons.</param>
	///
	/// <param name="columnIndex">The index within <pr>innerGrid</pr> that the two boundary lines
	/// 	will be created incontains tab buttons.</param>
	///
	/// <param name="itemIsSelected"><n>true</n> if the boundary lines are to be displayed the way
    ///     they appear for a tab (item) that's selected; <n>false</n> if for a tab that's not
    ///     selected or for the gap to the left of the first tab or the right of the last tab.
    ///     </param>
	///
    void AdjustBoundaryLinesForColumn(Grid innerGrid, int columnIndex, bool itemIsSelected)
    {
        // set <upperBoundaryLine> to the upper (e.g. light-colored) boundary line; create it if
		// needed
        Rectangle upperBoundaryLine;
		if (columnIndex < _upperBoundaryLines.Count)
			upperBoundaryLine = _upperBoundaryLines[columnIndex];
		else
		{
			upperBoundaryLine = new Rectangle()
            {
                Style = _upperBoundaryStyle,
            };
			Grid.SetColumn(upperBoundaryLine, columnIndex);
            innerGrid.Children.Add(upperBoundaryLine);
			_upperBoundaryLines.Add(upperBoundaryLine);
        }

		// adjust the properties of <upperBoundaryLine> -- it's always visible, but its height
		// differs depend on whether it's selected (basically, if the lower boundary line is
		// visible in front of it then it's two unit tall, otherwise it's one unit tall)
		upperBoundaryLine.Height = itemIsSelected ? 1 : 2;

        // set <lowerBoundaryLine> to the lower (e.g. dark-colored) boundary line; create it if
		// needed
        Rectangle lowerBoundaryLine;
		if (columnIndex < _lowerBoundaryLines.Count)
			lowerBoundaryLine = _lowerBoundaryLines[columnIndex];
		else
		{
			lowerBoundaryLine = new Rectangle()
            {
                Style = _lowerBoundaryStyle,
            };
			Grid.SetColumn(lowerBoundaryLine, columnIndex);
            innerGrid.Children.Add(lowerBoundaryLine);
			_lowerBoundaryLines.Add(lowerBoundaryLine);
        }


        if (!itemIsSelected || IsPartiallySelected || !TabBottomSeparatorIsSuppressed)
            upperBoundaryLine.Visibility = Visibility.Visible;
        else
            upperBoundaryLine.Visibility = Visibility.Collapsed;

        /*
        if (itemIsSelected && !IsPartiallySelected && TabBottomSeparatorIsSuppressed)
            upperBoundaryLine.Visibility = Visibility.Collapsed;
        else
            upperBoundaryLine.Visibility = Visibility.Visible;
        */

		// adjust the properties of <lowerBoundaryLine> -- it's visible when the item isn't
		// selected, or the item is "partially selected"
        if (!itemIsSelected || IsPartiallySelected)
			lowerBoundaryLine.Visibility = Visibility.Visible;
		else
			lowerBoundaryLine.Visibility = Visibility.Collapsed;
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="innerGrid">TODO</param>
    ///
    /// <param name="boundaryLines">TODO</param>
    ///
	void DeleteBoundaryLines(Grid innerGrid, List<Rectangle> boundaryLines)
	{
		foreach (Rectangle boundaryLine in boundaryLines)
			innerGrid.Children.Remove(boundaryLine);
		boundaryLines.Clear();
	}

    void PerformLayout()
    {
        _pendingLayout = false;

        // set <innerGrid> to the Grid created by the ItemsPresenter control -- this is the Grid
        // where the entire tab strip is displayed
        var innerGrid = (Grid) VisualTreeHelper.GetChild(_itemsPresenter, 0);

        // if the mapping between tabs and <_upperBoundaryLines> and <_lowerBoundaryLines> is no
		// longer correct, remove all boundary lines (i.e. the light and dark lines at the bottom
		// of the control) -- they will be re-created by calls to AdjustBoundaryLinesForColumn
		// below
        if (_needToDeleteBoundaryLines)
		{
			DeleteBoundaryLines(innerGrid, _upperBoundaryLines);
			DeleteBoundaryLines(innerGrid, _lowerBoundaryLines);
            _needToDeleteBoundaryLines = false;
		}

        // <innerGrid> is set to be slightly taller than the outer grid (<_layoutRoot>) so that
        // the outer grid clips the bottoms of the buttons that are used as tabs
        innerGrid.Height = Height + _bottomTruncation;

        innerGrid.ColumnDefinitions.Clear();

        // draw the left gap
        int columnIndex = 0;
        innerGrid.ColumnDefinitions.Add(new ColumnDefinition()
        {
            Width = LeftGapWidth
        });
        AdjustBoundaryLinesForColumn(innerGrid, columnIndex, false);
        columnIndex++;

        // draw the tabs, and the boundary lines beneath them
		bool beforeSelectedItem = true, afterSelectedItem = false;
        object selectedItemData = SelectedItemData;
        double itemMinWidth = ItemMinWidth;
        Thickness itemPadding = ItemPadding;
		bool isPartiallySelected = IsPartiallySelected;
		foreach (object item in Items)
		{
            TouchButton container = _mapItemToContainer[item];

            // skip items marked as Visibility="Collapsed"
            if (container.Visibility == Visibility.Collapsed)
                continue;

            innerGrid.ColumnDefinitions.Add(new ColumnDefinition()
            {
                Width = GridLength.Auto
            });

            string itemData = Item.GetUntypedValue(item);
			bool itemIsSelected = (itemData != null) && Object.Equals(itemData, selectedItemData);

			if (itemIsSelected)
				beforeSelectedItem = false;

            container.IsLit = itemIsSelected && !isPartiallySelected;
            container.MinWidth = itemMinWidth;
            container.Padding = itemPadding;

			if (beforeSelectedItem)
				container.Margin = _beforeSelectedTabButtonMargin;
			else
			if (afterSelectedItem)
				container.Margin = _afterSelectedTabButtonMargin;
			else
				container.Margin = _selectedTabButtonMargin;

			container.SetValue(Grid.ColumnProperty, columnIndex);

            AdjustBoundaryLinesForColumn(innerGrid, columnIndex, itemIsSelected);

			if (itemIsSelected)
				afterSelectedItem = true;

            columnIndex++;
		}

        // draw the right gap
        innerGrid.ColumnDefinitions.Add(new ColumnDefinition()
        {
            Width = new GridLength(1, GridUnitType.Star)
        });
        AdjustBoundaryLinesForColumn(innerGrid, columnIndex, false);
        columnIndex++;

        // remove any unused boundary line controls from <innerGrid>
        _upperBoundaryLines.TrimExcess();
        _lowerBoundaryLines.TrimExcess();
    }

    // IControlHelperConnector implementation

    Control IControlHelperConnector.GetControl()
    {
        return this;
    }

    DependencyObject IControlHelperConnector.GetTemplateChild(string childName)
    {
        return GetTemplateChild(childName);
    }
}

/*
// only for internal use by TouchListBox
public class TouchTabStripItem : TouchButton
{
}
*/

}

